﻿//===================================================================================
// SPrello - sprello.codeplex.com
// A SharePoint ui inspired by trello.com
//===================================================================================
// Copyright (c) 2011-14 Pentalogic Technology Ltd. All rights reserved.
// THIS CODE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================

/* See also Notes.txt for terminoligy

/* === SharePoint Client Object Model methods */

function SPrello(webPartId,
                listName, viewName,
                categoryFieldName, orderFieldName,
                titleFieldName, detailFieldName,
                onCardClick, allowEditing, logToConsole) {
    // Create a new SPrello object and set it up

    // The console can be found in IE and Chrome developer tools (F12) and Firfox with Firebug add on
    this.LogToConsole = logToConsole;
    if (!window.console)
        this.LogToConsole = false;

    this.LogMessage("Setting up new SPrello obj :-");
    this.LogMessage("- webPartid:" + webPartId);
    this.LogMessage("- listName:" + listName);
    this.LogMessage("- viewName:" + viewName);
    this.LogMessage("- categoryFieldName:" + categoryFieldName);
    this.LogMessage("- orderFieldName:" + orderFieldName);
    this.LogMessage("- titleFieldName:" + titleFieldName);
    this.LogMessage("- detailFieldName:" + detailFieldName);
    this.LogMessage("- onCardClick:" + onCardClick);
    this.LogMessage("- allowEditing:" + allowEditing);

    this.MIN_LIST_WIDTH = 200;
    this.MAX_LIST_WIDTH = 600;

    // Public properties
    this.WebPartId = webPartId;
    this.ListName = listName;
    this.ViewName = viewName;
    this.CategoryFieldName = categoryFieldName;
    if (orderFieldName.length > 0)
        this.OrderFieldName = orderFieldName;
    this.TitleFieldName = titleFieldName;
    if (detailFieldName.length > 0)
        this.DetailFieldName = detailFieldName;
    this.OnCardClick = onCardClick;
    this.AllowEditing = allowEditing;
    this.setup();

    // Setup reference to this object
    // Todo - pass in reference to main div to be able to have multiple boards on one page
    $("#board").data("SPrello", this);
}

SPrello.prototype.LogMessage = function (message) {
    if (this.LogToConsole)
        console.log(message);
};

SPrello.prototype.LogError = function (message) {
    if (this.LogToConsole)
        console.error(message);
};

SPrello.prototype.setup = function () {
    // Setup the SPrello object

    // Setup context & load web
    context = new SP.ClientContext.get_current();

    web = context.get_web();
    context.load(web, "Title", "Id");

    // Load task list
    this.List = web.get_lists().getByTitle(this.ListName);
    // context.load allows us to specify only to get the properties we're interested in - keeps traffic down.
    // http://msdn.microsoft.com/en-us/library/ee539350.aspx
    context.load(this.List, "DefaultDisplayFormUrl", "DefaultEditFormUrl", "DefaultNewFormUrl");
    
    if (this.ViewName.length > 0) {
        // If we have a view then we have to call the server to get the CAML for the view
        // http://sharepoint.stackexchange.com/questions/28446/how-to-get-all-items-in-a-view-using-client-object-model-javascript
        this.View = this.List.get_views().getByTitle(this.ViewName);
        context.load(this.View);        
        this.LogMessage("Calling server to get web and list");
        this.WaitMessageid = SP.UI.Notify.addNotification("<img src='/_layouts/images/loadingcirclests16.gif'> Finding info...", true);
        context.executeQueryAsync(Function.createDelegate(this, this.setup_getView_OnSuccess), Function.createDelegate(this, this.OnFailure));
    }
    else {
        // If not view then we can just jump directly to setup_getListItems and have just one call to the server
        this.setup_getListItems(SP.CamlQuery.createAllItemsQuery());
    }
}

SPrello.prototype.setup_getView_OnSuccess = function (sender, args) {
    // Optional Async callback from .setup if we need to get a View.
    SP.UI.Notify.removeNotification(this.WaitMessageid)
    this.LogMessage("Got view " + this.ViewName + " CAML:" + this.View.get_viewQuery());
    // Now call a 2nd time to get list items now we have the view caml
    var query = new SP.CamlQuery();
    query.set_viewXml("<View><Query>" + this.View.get_viewQuery() + "</Query></View>");
    this.setup_getListItems(query);
}
    
SPrello.prototype.setup_getListItems = function (camlQuery) {
    // Continue to get all the list items matching the given SP.CamlQuery
    this.ListItems = this.List.getItems(camlQuery);

    context = new SP.ClientContext.get_current();
    context.load(this.ListItems);

    // Note - to avoid problems with reserved name or titles being different from internal names
    // Get fields by Title as that matches what user put in, but get/update values using internal names
    // E.g. Order is reserved so if a user puts a field in called Order it will have an internal name called Order0

    var getFields = this.List.get_fields();

    this.CategoryField = context.castTo(getFields.getByTitle(this.CategoryFieldName), SP.FieldChoice);
    context.load(this.CategoryField);

    if (this.OrderFieldName != null) {
        this.OrderField = getFields.getByTitle(this.OrderFieldName);
        context.load(this.OrderField);
    }

    // In SharePoint 2013 the Title field is now renamed "Task Name" in some lists. So in that case get it by
    // the internal name (which is still title).    
    if (this.TitleFieldName == "Title")
        this.TitleField = this.List.get_fields().getByInternalNameOrTitle(this.TitleFieldName);
    else
        this.TitleField = getFields.getByTitle(this.TitleFieldName);
    context.load(this.TitleField);

    if (this.DetailFieldName != null) {
        this.DetailField = getFields.getByTitle(this.DetailFieldName);
        context.load(this.DetailField);
    }

    // Call server
    this.LogMessage("Calling server to get web and list");
    this.WaitMessageid = SP.UI.Notify.addNotification("<img src='/_layouts/images/loadingcirclests16.gif'> Finding info...", true);
    context.executeQueryAsync(Function.createDelegate(this, this.setup_getListItems_OnSuccess), Function.createDelegate(this, this.OnFailure));
}

SPrello.prototype.setup_getListItems_OnSuccess = function (sender, args) {
    // Async callback from setup_getListItems
    this.SendNotification("Operation succeeded...");

    // Read current web stuff - not sure if we need
    this.LogMessage('Opened spWeb:' + web.get_title() + '\n ID:' + web.get_id());

    // If choice field add a list for each choice
    if (this.CategoryField.get_typeAsString() == "Choice") {
        var choices = this.CategoryField.get_choices();
        this.LogMessage("CategoryField values: (" + choices.length + ") - " + choices.join(","));
        for (i = 0; i < choices.length; i++) {
            this.AddList(choices[i], choices[i]);
        }
    }

    // Read task list items
    this.LogMessage("Got List Items :-");
    var enumerator = this.ListItems.getEnumerator();
    while (enumerator.moveNext()) {
        var listItem = enumerator.get_current();
        this.AddListItemAsCard(listItem);
    }
};

SPrello.prototype.GetFieldValue = function (field, listItem) {
    var fieldType = field.get_typeAsString();
    var fieldValue = listItem.get_item(field.get_internalName());
    if (fieldValue != null && (fieldType == "User" || fieldType == "Lookup"))
        fieldValue = fieldValue.get_lookupValue();
    return fieldValue;
};


SPrello.prototype.AddListItemAsCard = function (listItem) {
    // Adds the specified list item as a card, ensuring that the List (cateory) is present.
    var order = 0;
    if (this.OrderField != null)
        order = listItem.get_item(this.OrderField.get_internalName());

    this.LogMessage(' - ID: ' + listItem.get_id().toString() + ' Title: ' + listItem.get_item('Title') + ' Order: ' + order + '<br>');

    var categoryValue = this.GetFieldValue(this.CategoryField, listItem);

    var list = this.GetList(categoryValue)
    // Need to make allowance for fill-in choices (may not have these yet), lookups, users etc if so then add.
    if (list == null) {
        var catType = this.CategoryField.get_typeAsString();

        var lookupId = null;
        if (catType == "Lookup" || catType == "User") {
            var lookupValue = listItem.get_item(this.CategoryField.get_internalName());
            if (lookupValue != null)
                lookupId = lookupValue.get_lookupId();
        }

        list = this.AddList(categoryValue, categoryValue, lookupId);
    }

    var detail = null;
    if (this.DetailField != null) {
        var detail = this.GetFieldValue(this.DetailField, listItem);
    }

    // If we have an order field then be sure to add the card in order.
    // We could add this to the CAML when getting the list items but that puts extra load on the server (rather than client)
    // and we would then have to figure out which order the lists (1-High, 2-Med) etc should go in.
    this.AddCard(list, order, listItem.get_id(),
                 this.GetFieldValue(this.TitleField, listItem),
                 detail);
};

SPrello.prototype.RefreshCard = function () {
    // Called to reload a card    
    this.LogMessage("RefreshCard - ID=" + this.LastCardEdited.data("listId"));

    this.RefreshItem = this.List.getItemById(this.LastCardEdited.data("listId"));
    context.load(this.RefreshItem);

    this.LogMessage("Refreshing card");
    this.WaitMessageid = SP.UI.Notify.addNotification("<img src='/_layouts/images/loadingcirclests16.gif'> Refreshing card...", true);
    context.executeQueryAsync(Function.createDelegate(this, this.RefreshCard_OnSucccess), Function.createDelegate(this, this.OnFailure));
};

SPrello.prototype.RefreshCard_OnSucccess = function (sender, args) {
    this.LogMessage("RefreshCard_OnSucccess");
    this.SendNotification("Operation succeeded...");

    // Add in refreshed card
    this.AddListItemAsCard(this.RefreshItem);
    this.RefreshItem = null;

    // Remove old card
    this.LastCardEdited.remove();
    this.LastCardEdited = null;
}

SPrello.prototype.UpdateCard = function (card) {
    // Called after a card is moved - updates SharePoint list using COM

    var mainItemId = card.data("listId");
    var list = card.closest("div.sprello-list");
    var categoryValue = list.data("category")
    // Only need categoryLookupId for User/Lookups so may be null
    var categoryID = list.data("categoryID")

    this.LogMessage("Updating ID: " + mainItemId + " to " + categoryValue);

    var mainItemOrder = 0;

    // If we have an order field to use then work out both what this item's order ID should be
    // and update all other items in that column at same time
    if (this.OrderField != null) {
        var cards = card.parent().children();
        for (i = 0; i < cards.length; i++) {
            var itemId = $(cards[i]).data("listId");
            if (itemId == mainItemId) {
                mainItemOrder = i + 1;
            }
            else {
                var item = this.List.getItemById(itemId);
                item.set_item(this.OrderField.get_internalName(), i + 1);
                item.update();
            }
        }
    }

    var mainItem = this.List.getItemById(mainItemId);

    var categoryFieldType = this.CategoryField.get_typeAsString();
    if (categoryFieldType == "Lookup" || categoryFieldType == "User") {
        // Set lookup's using ID
        mainItem.set_item(this.CategoryField.get_internalName(), new SP.FieldLookupValue().set_lookupId(categoryID));
    }
    else {
        mainItem.set_item(this.CategoryField.get_internalName(), categoryValue);
    }

    if (mainItemOrder != 0)
        mainItem.set_item(this.OrderField.get_internalName(), mainItemOrder);

    mainItem.update();

    this.WaitMessageid = SP.UI.Notify.addNotification("<img src='/_layouts/images/loadingcirclests16.gif'> Updating card...", true);
    context.executeQueryAsync(Function.createDelegate(this, this.ListItemUpdate_onSuccess), Function.createDelegate(this, this.OnFailure));
};

SPrello.prototype.ListItemUpdate_onSuccess = function (sender, args) {
    // Sort out notifcations dialogs after successfull update of a card
    this.SendNotification("Update succeeded...");
};


/* === Notifiation routines */

SPrello.prototype.SendNotification = function (message) {
    // Sort out notifcations dialogs after successfull initial load
    SP.UI.Notify.removeNotification(this.WaitMessageid)
    SP.UI.Notify.addNotification(message, false);
}

SPrello.prototype.OnFailure = function (sender, args) {
    // Sort out notifcations dialogs after failure to update card    
    if (this.WaitMessageid != null)
        SP.UI.Notify.removeNotification(this.WaitMessageid)
    var message = args.get_message();
    this.WaitMessageid = SP.UI.Notify.addNotification("<img src='/_layouts/images/error16by16.gif'> Error : " + message, true);
    this.LogError('Request failed ' + message + '\n' + args.get_stackTrace());
};

/* === jQuery UI methods */

// Using Sortable list - http://jqueryui.com/demos/sortable/

SPrello.prototype.AddList = function (category, title, id) {
    // Create and add a new list to the board

    var board = $("#board");
    var list = $(this.GenerateList(title))
    list.data("category", category);
    if (id != null)
        list.data("categoryID", id);
    board.append(list);

    var numLists = board.children().length;

    // Try and fit lists equally into horiz space, but if would be too narrow then remove one
    // (will be placed below) and retry again
    var listWidth = 0;
    do {
        listWidth = (board.innerWidth() / numLists);
        numLists--;
    }
    while (listWidth < this.MIN_LIST_WIDTH);

    if (listWidth > this.MAX_LIST_WIDTH)
        listWidth = this.MAX_LIST_WIDTH;

    $("div#board div.sprello-list").outerWidth(listWidth);

    list.find("ul.listSortable").sortable({
        connectWith: ".listSortable",
        update: this.Sortable_onUpdate
    });
    return list;
};

SPrello.prototype.GenerateList = function (title) {
    // Create and return a new list

    return "<div class='sprello-list'><div class='sprello-board-chrome'><div class='sprello-board-inner'>" +
                "<div class='sprello-board-title'>" + title + "</div>" +
                "<ul class='listSortable'></ul>" +
                "<div class='sprello-board-contents'></div>" +
            "</div></div></div>";
};

SPrello.prototype.GetList = function (category) {
    // Gets the specified list

    var lists = $("div.sprello-list");
    for (i = 0; i < lists.length; i++) {
        if ($(lists[i]).data("category") == category)
            return lists[i];
    }
};

SPrello.prototype.AddCard = function (list, order, listId, title, detail) {
    // Create and add a new card to the specified list

    if (typeof (list) == "string")
        list = this.GetList(list);

    if (list != null) {

        var ul = $(list).find("ul");
        var cards = ul.children();

        if (ul != null) {
            if (order == 0 || order > cards.length)
                ul.append(this.GenerateCard(listId, order, title, detail));
            else {
                // We have to insert this in the correct position. Can't just use jQuery position selectors as
                // there may be gaps in list if card been deleted - e.g. 1, 3, 4 then we won't be putting card 3 in position 3!
                // Instead do it brute force way - could improve this if neccessary (binary search at very least) or put into 
                // array and use some sorting lib's like in backbone.js
                var newCard = this.GenerateCard(listId, order, title, detail);
                for (var i = 0; i < cards.length; i++) {
                    card = $(cards[i]);
                    if (order < card.data("order")) {
                        card.before(newCard);
                        newCard = null;
                        break;
                    }
                }
                // If didn't slot it in, append to end.
                if (newCard != null)
                    ul.append(newCard);
            }
        }
    }
};

SPrello.prototype.GenerateCard = function (listId, order, title, detail) {
    // Create and return a new card

    var cardHtml = "<li><div class='sprello-card'>" +
                    "<div class='sprello-title'>" + title + "</div>";
    if (detail != "" && detail != null)
        cardHtml += "<div class='sprello-detail'>" + detail + "</div>";
    var card = $(cardHtml + "</div></li>");

    card.data("listId", listId);
    card.data("order", order);

    if (this.OnCardClick != "Nothing")
        card.click(this.Card_onClick);

    return card;
};

SPrello.prototype.Card_onClick = function () {
    // Called OnClick of a card - open view/edit form
    // As called from jQuery onlick don't have SPRello object context - so go find it.
    sprello = $("#board").data("SPrello");
    if (sprello != null) {
        // Open in list display form in dialog - http://msdn.microsoft.com/en-us/library/ff410058.aspx
        var options = {};
        if (sprello.OnCardClick == "Edit_Form")
            options.url = sprello.List.get_defaultEditFormUrl();
        else
            options.url = sprello.List.get_defaultDisplayFormUrl();

        // If we don't append ?ID=X onto end of url then won't have context of edited record
        // so won't enabled ribbon Edit button and a save will throw 500 server error
        itemId = $(this).data("listId");
        options.url = options.url + "?ID=" + itemId;

        // Don't have any context in dialogReturnValueCallback so have to store ID of last edit here
        sprello.LastCardEdited = $(this);

        options.dialogReturnValueCallback =
                  function (dialogResult, returnValue) {
                      sprello = $("#board").data("SPrello");
                      if (sprello != null) {
                          // dialogResult will be 0 if cancel/close etc - 1 if save, add - so only refresh on 1.
                          if (dialogResult)
                              sprello.RefreshCard();
                          else
                              sprello.LastCardEdited = null;
                      }
                  };

        // Display the dialog box.
        SP.UI.ModalDialog.showModalDialog(options);
    }
}

SPrello.prototype.AddNewCard = function () {
    this.LogMessage("AddNewCard url:" + this.List.get_defaultNewFormUrl());
    var options = {};
    options.url = this.List.get_defaultNewFormUrl();
    options.dialogReturnValueCallback =
                  function (dialogResult, returnValue) {
                      // Lost sprello context again as callback function
                      sprello = $("#board").data("SPrello");
                      if (sprello != null) {
                          sprello.LogMessage("AddNewCard_CallBack : result=" + dialogResult + " returnValue=" + returnValue);
                          // Waiting to see if there is a good method for getting the ID of the record inserted so we can use
                          // RefreshCard code - in mean time just go nuclear and clear out all cards and re-add
                          // http://sharepoint.stackexchange.com/questions/28280/how-to-get-the-id-of-the-list-item-just-inserted-using-sp-ui-modaldialog-showmod
                          if (dialogResult) {
                              $("#board").empty();
                              sprello.setup();
                          }
                      }
                  }
    SP.UI.ModalDialog.showModalDialog(options);
};

SPrello.prototype.Sortable_onUpdate = function (event, ui) {
    // Called when a card is moved
    // Will fire twice, onen for list we've left and once for new list - ignore first.

    if (ui.sender == null) {
        sprello = $("#board").data("SPrello");
        if (sprello != null) {
            sprello.UpdateCard(ui.item);
        }
    }
};